Задача проекта: проанализировать по статистическим данным транспортную безопасность. В качестве района исследования взяли Екатеринбург. Данные о ДТП - из открытого источника - Сайт некоммерческого проекта «Карта ДТП»
Тех задание
# импорт необходимых библиотек
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.ticker import (MultipleLocator, AutoMinorLocator)
import matplotlib.ticker as ticker
import seaborn as sns
import calendar
import geopandas as gpd
from geopy.geocoders import Nominatim
from shapely.geometry import Polygon
import folium
from folium.plugins import MarkerCluster, HeatMap, HeatMapWithTime
import requests
import json
# сделаем таблицу из файла "плоской"
with open('/Users/09e6y/Desktop/Катькис/geoanalys/sverdlovskaia-oblast.geojson', 'r') as j:
contents = json.loads(j.read())
df_history = gpd.GeoDataFrame.from_features(contents)
df_history.head()
| geometry | id | tags | light | point | nearby | region | scheme | address | weather | ... | datetime | severity | vehicles | dead_count | participants | injured_count | parent_region | road_conditions | participants_count | participant_categories | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | POINT (61.27986 56.41228) | 2620257 | [Дорожно-транспортные происшествия] | В темное время суток, освещение отсутствует | {'lat': 56.412281, 'long': 61.279861} | [Жилые дома индивидуальной застройки, Одиночны... | Каменский | 880 | п Ленинский, ул Советская, 10б | [Пасмурно] | ... | 2021-09-26 03:00:00 | Легкий | [{'year': 2013, 'brand': 'ВАЗ', 'color': 'Сини... | 0 | [{'role': 'Пешеход', 'gender': 'Женский', 'vio... | 1 | Свердловская область | [Сухое, Отсутствие тротуаров (пешеходных дорож... | 2 | [Все участники, Пешеходы] |
| 1 | POINT (61.84509 56.35430) | 2620260 | [Дорожно-транспортные происшествия] | В темное время суток, освещение отсутствует | {'lat': 56.354302, 'long': 61.845088} | [] | Каменский | 010 | Р-354 Екатеринбург - Шадринск - Курган, 101 км | [Ясно] | ... | 2021-09-24 04:35:00 | С погибшими | [{'year': 2019, 'brand': 'ГАЗ', 'color': 'Серы... | 2 | [] | 1 | Свердловская область | [Сухое] | 4 | [Все участники] |
| 2 | POINT (61.60253 56.47734) | 2645956 | [Дорожно-транспортные происшествия] | Светлое время суток | {'lat': 56.477342, 'long': 61.60253} | [Нерегулируемый перекрёсток неравнозначных ули... | Каменский | 600 | с.Покровское - с.Кисловское - д.Соколова, 1 км | [Ясно] | ... | 2021-11-23 15:40:00 | Тяжёлый | [{'year': 2007, 'brand': 'FORD', 'color': 'Сер... | 0 | [] | 2 | Свердловская область | [Обработанное противогололедными материалами] | 2 | [Все участники] |
| 3 | POINT (61.92576 56.38442) | 2645970 | [Дорожно-транспортные происшествия] | В темное время суток, освещение включено | {'lat': 56.384422, 'long': 61.925758} | [Многоквартирные жилые дома, Остановка обществ... | Каменский | 070 | г Каменск-Уральский, ул Суворова, 38 | [Пасмурно] | ... | 2021-11-19 17:45:00 | Тяжёлый | [{'year': 2012, 'brand': 'HYUNDAI', 'color': '... | 0 | [] | 1 | Свердловская область | [Обработанное противогололедными материалами, ... | 3 | [Все участники] |
| 4 | POINT (61.42954 56.52484) | 2645973 | [Дорожно-транспортные происшествия] | Светлое время суток | {'lat': 56.524844, 'long': 61.429539} | [Остановка общественного транспорта, Нерегулир... | Каменский | 600 | Р-354 Екатеринбург - Шадринск - Курган, 67 км | [Пасмурно] | ... | 2021-11-17 07:45:00 | Тяжёлый | [{'year': 2021, 'brand': 'HYUNDAI', 'color': '... | 0 | [] | 3 | Свердловская область | [Сухое] | 3 | [Все участники] |
5 rows × 21 columns
display(df_history['datetime'].min())
df_history['datetime'].max()
'2015-01-01 03:00:00'
'2022-11-30 22:20:00'
Добавим колонки, которые понадобятся в дальнейшем анализе.
df_history['datetime'] = pd.to_datetime(df_history['datetime']) # преобразуем формат времени
df_history['year'] = df_history['datetime'].apply(lambda x: pd.to_datetime(x).year) # выделим год в отдельную колонку
df_history['month'] = df_history.datetime.apply(lambda x: (x).month)
df_history['month'].value_counts()
9 2377 8 2330 7 2281 10 2268 11 2129 6 2060 12 2016 5 1775 1 1689 2 1435 3 1369 4 1352 Name: month, dtype: int64
Необходимо выделить данные, посвященные только Екб и за актуальный период - 2022 год.
# сделаем срез
df = df_history.query('region == "Екатеринбург" and year > 2021').reset_index(drop=True)
# пол виновника
def get_sex(cell):
try:
return (cell[0]['participants'][0]['gender'])
except: return np.nan
df['sex'] = df.vehicles.apply(get_sex)
# стаж вождения
def get_stage(cell):
try:
return (cell[0]['participants'][0]['years_of_driving_experience'])
except: return np.nan
df['stage'] = df.vehicles.apply(get_stage)
# характер полученных травм
def get_status(cell):
try:
return (cell[0]['participants'][0]['health_status'])
except: return np.nan
df['health_status'] = df.vehicles.apply(get_status)
df['health_status']
0 Не пострадал
1 Не пострадал
2 Раненый, находящийся (находившийся) на амбулат...
3 Не пострадал
4 Не пострадал
...
1053 Не пострадал
1054 Не пострадал
1055 Не пострадал
1056 Не пострадал
1057 Не пострадал
Name: health_status, Length: 1058, dtype: object
df['weekday'] = df['datetime'].dt.dayofweek
df['weekday'] = df['weekday'].apply(lambda x:calendar.day_name[x])
df['weekday']
0 Wednesday
1 Tuesday
2 Wednesday
3 Wednesday
4 Wednesday
...
1053 Friday
1054 Thursday
1055 Sunday
1056 Saturday
1057 Thursday
Name: weekday, Length: 1058, dtype: object
df.info()
<class 'geopandas.geodataframe.GeoDataFrame'> RangeIndex: 1058 entries, 0 to 1057 Data columns (total 27 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 geometry 1058 non-null geometry 1 id 1058 non-null int64 2 tags 1058 non-null object 3 light 1058 non-null object 4 point 1058 non-null object 5 nearby 1058 non-null object 6 region 1058 non-null object 7 scheme 1046 non-null object 8 address 977 non-null object 9 weather 1058 non-null object 10 category 1058 non-null object 11 datetime 1058 non-null datetime64[ns] 12 severity 1058 non-null object 13 vehicles 1058 non-null object 14 dead_count 1058 non-null int64 15 participants 1058 non-null object 16 injured_count 1058 non-null int64 17 parent_region 1058 non-null object 18 road_conditions 1058 non-null object 19 participants_count 1058 non-null int64 20 participant_categories 1058 non-null object 21 year 1058 non-null int64 22 month 1058 non-null int64 23 sex 1020 non-null object 24 stage 776 non-null float64 25 health_status 1058 non-null object 26 weekday 1058 non-null object dtypes: datetime64[ns](1), float64(1), geometry(1), int64(6), object(18) memory usage: 223.3+ KB
Пропуски - в адресах, заполним их с помощью geopy по координатам. Для этого нам понадобятся отдельные колонки с широтой и долготой.
df['long'] = df['point'].apply(lambda x: x['long']) # долгота
df['lat'] = df['point'].apply(lambda x: x['lat']) # широта
# как это работает
geolocator = Nominatim(user_agent="my-appln")
location = geolocator.reverse("56.853536, 60.615284")
print(location.address)
df['coordinates'] = df['lat'].astype('str') + ',' + df['long'].astype('str')
36, улица Луначарского, Малевич, Железнодорожный район, Екатеринбург, городской округ Екатеринбург, Свердловская область, Уральский федеральный округ, 620047, Россия
# заполним пропуски в адресах по координатам при помощи geolocator
def fill_address(row):
if row['address']==None:
return geolocator.reverse(row['coordinates'])
return row['address']
df['address'] = df.apply(fill_address, axis=1)
Проверим на дубликаты.
df.duplicated(['id','coordinates']).sum()
0
Т.к. у нас нет информации о районах, а мы бы хотели отразить их на карте. Достанем информацию о районах из внешних источниках, соберем их в список. Данные для границ полигонов возьмем из api open street maps.
districts = ['Академический', 'Верх-Исетский', 'Железнодорожный', 'Кировский', 'Ленинский', 'Октябрьский', 'Орджоникидзевский', 'Чкаловский']
lst_d = []
lst_poly = []
# в цикле обратимся к api `open street maps` для каждого района
for d in districts:
dis = json.loads(requests.get(f"http://nominatim.openstreetmap.org/search?q={d} район Екатеринбург&polygon_geojson=1&format=json").text)[0]
lst_sets = []
for elem in dis['geojson']['coordinates'][0]:
lst_sets.append(tuple(elem))
lst_d.append(d)
lst_poly.append(Polygon(lst_sets))
df_polygons = pd.DataFrame()
df_polygons['district'] = lst_d
df_polygons['geometry'] = lst_poly
ekb_map = folium.Map(location=[56.8519, 60.6122], zoom_start = 10)
for _, d in df_polygons.iterrows():
sim_geo = gpd.GeoSeries(d['geometry']).simplify(tolerance=0.001)
geo_j = sim_geo.to_json()
geo_j = folium.GeoJson(data=geo_j,
style_function=lambda x: {'fillColor': 'orange'})
folium.Popup(d['district']).add_to(geo_j)
geo_j.add_to(ekb_map)
ekb_map
Мы добавили недостающие адреса и нарисовали карту районов. Можно перейти к исследованию данных.
Как много ДТП произошло в Екатеринбурге с 2021 года?
print('Общее число происшествий в 2021-2022 годах: {}'.format(df.shape[0]))
Общее число происшествий в 2021-2022 годах: 1058
# построим гистограмму распределения событий по дням
fig, ax=plt.subplots(figsize=(15, 5), dpi=150)
ax=sns.histplot(data=df, x='datetime', bins=200)
plt.title('Распределение аварий по дате и времени')
plt.ylabel('Количество аварий');
Трудно привязать пики к каким-то конкретным сезонным данным (как бы ни хотелось), у нас слишком мало данных, и 14 аварий - может быть и пиковое значение, но это всего 14 на 1,4 миллиона жителей, то есть 1 на 100 000.
accidents_month = (
df.groupby(df['month'])
.agg(ac_count=('id', 'count'))
)
accidents_month.index=[calendar.month_name[x] for x in range(1,12)]
accidents_month.reset_index(inplace=True)
accidents_month.columns=['month', 'ac_count']
clrs = ['#bcbcbc' if x > 81 else '#5a9bc5' for x in accidents_month['ac_count'] ]
sns.barplot(data=accidents_month, y = 'month', x='ac_count', palette=clrs)
plt.title('Распределение количества ДТП по месяцам')
plt.ylabel('')
plt.xlabel('количество');
plt.savefig('по_месяцам.png', dpi=90, bbox_inches='tight')
Видим постепенный рост числа аварий с марта, пик - в августе и спад - начиная с октября, но в среднем количество аварий стабильно между 70 и 100 в месяц. Можем продемонстрировать распределение на боксплоте.
sns.boxplot(x = accidents_month.ac_count)
plt.title('Распределение числа ДТП')
plt.xlabel('')
plt.savefig('распределение.png');
Сравним экстремальные данные за март с нашим распределением.
display(accidents_month['ac_count'].median())
accidents_month.query('month == "March"')['ac_count']
98.0
2 105 Name: ac_count, dtype: int64
Посмотрим на исторические данные.
accidents_month_h = (
df_history
.groupby(df_history['month'])
.agg(ac_count=('id', 'count'))
)
accidents_month_h.index=[calendar.month_name[x] for x in range(1,13)]
accidents_month_h.reset_index(inplace=True)
accidents_month_h.columns=['month', 'ac_count']
clrs = ['#bcbcbc' if (x > 1500) else '#5a9bc5' for x in accidents_month_h['ac_count'] ]
sns.barplot(data=accidents_month_h, y = 'month', x='ac_count', palette=clrs) #, color='#5a9bc5'
plt.title('В начале года - спад ДТП')
plt.ylabel('')
plt.xlabel('количество')
plt.savefig('по_месяцам_история.png', dpi=90, bbox_inches='tight');
Если взять все время, увидим значительный спад с февраля по апрель.
accidents_days = (
df.groupby(df['datetime']
.dt.dayofweek)
.agg(ac_count=('id', 'count'))
)
accidents_days.index=[calendar.day_name[x] for x in range(0,7)]
accidents_days.reset_index(inplace=True)
accidents_days.columns=['day', 'ac_count']
clrs = ['#5a9bc5' if (x == max(accidents_days.ac_count) or x == min(accidents_days.ac_count)) else '#bcbcbc' for x in accidents_days.ac_count ]
sns.barplot(data=accidents_days, y = 'day', x='ac_count', palette=clrs)
plt.title('Распределение количества ДТП по дням недели')
plt.ylabel('')
plt.xlabel('количество')
plt.savefig('по_дням_недели.png', dpi=90, bbox_inches='tight');
Почему-то больше всего аварий по средам, а меньше всего - по субботам.
import matplotlib.dates as md
accidents_hour = (
df.groupby(df['datetime']
.dt.hour)
.agg(ac_count=('id', 'count'))
.reset_index()
)
clrs = ['#bcbcbc' if (x > 20) else '#5a9bc5' for x in accidents_hour['ac_count']]
fig, ax = plt.subplots(figsize=(12,5), dpi=90)
ax.bar(accidents_hour.datetime, accidents_hour.ac_count, color=clrs)
ax.xaxis.set_major_locator(ticker.MultipleLocator(1))
ax.set_xlim(-0.5, 23.5)
plt.title('Распределение количества ДТП по времени суток')
plt.ylabel('')
plt.xlabel('количество')
plt.savefig('время.png', dpi=90, bbox_inches='tight');
Пиковое время: 7:00, 11:00 и вечерний час-пик с 17:00 до 19:00, ночью - затишье.
Посмотрим, как зависит тяжесть аварий от вермени.
accidents_hour_day = (
df.groupby([(df['datetime']
.dt.hour), 'weekday'])
.agg({'id':'count'})
.reset_index()
)
fig, ax=plt.subplots(figsize=(15, 5), dpi=150)
ax=sns.barplot(
data = accidents_hour_day,
y = 'id',
x='datetime',
hue='weekday',
palette='Spectral'
)
plt.title('Зависимость аварий от времени суток и дня недели')
plt.ylabel('Количество аварий')
plt.xlabel('Время');
Ночью в входные дни чуть больше вероятность аварий, чем ночью в будни.
В какое время года больше всего аварий?
def season(row):
month = row['month']
if 1 <= month <= 2 or month == 12:
return 'winter'
elif 3 <= month <= 5:
return 'spring'
elif 6 <= month <= 8:
return 'summer'
return 'autumn'
df['season'] = df.apply(season, axis = 1)
df['season'].value_counts()
summer 321 autumn 294 spring 291 winter 152 Name: season, dtype: int64
season_chart = (
df.groupby('season')
.agg({'id' : 'count'})
)
season_chart
| id | |
|---|---|
| season | |
| autumn | 294 |
| spring | 291 |
| summer | 321 |
| winter | 152 |
season_chart.plot(kind='pie', y = 'id', figsize=(5,5), autopct='%1.1f%%', legend = False)
plt.ylabel('')
plt.title('Распределение аварий по сезонам')
plt.savefig('сезоны_1.png', dpi=200, bbox_inches='tight');
Число аварий зимой чуть меньше, чем в остальное время года. С одной стороны, у нас не полные данные за декабрь, потому что в предновогодних пробках наверняка происходит много аварий, а с другой - в Екатеринбурге есть автомобильная чезонномть - часть людей ставит на автомобиль в гараж на зиму и пользуется общественным транспортом. Но чтобы проверить нашу теорию, посмотрим на данные за все время.
df_history['season'] = df_history.apply(season, axis = 1)
season_chart = (
df_history.groupby('season')
.agg({'id' : 'count'})
)
season_chart.plot(kind='pie', y = 'id', figsize=(5,5), autopct='%1.1f%%', legend = False)
plt.ylabel('')
plt.title('Распределение аварий по сезонам (исторические данные)')
plt.savefig('сезоны_2.png', dpi=200, bbox_inches='tight');
На осень и зиму приходится около 42% аварий.
df_history['month'] = df_history['datetime'].astype('datetime64[M]')
month_pivot = df_history.pivot_table(index = 'month', values = 'id', aggfunc='count').reset_index()
fig, ax=plt.subplots(figsize=(15, 5), dpi=150)
ax = sns.lineplot(data = month_pivot, x = 'month', y='id')
plt.title('Динамика числа аварий')
plt.xlabel('')
plt.ylabel('');
Очень живописный провал в каждом начале года, и особенно - в 2020. В целом кажется что аварий становится меньше.
Какие разновидности ДТП в наших данных и какие более распространены.
sns.histplot(df['severity'], stat="percent")
plt.title('Распределение ДТП по тяжести')
plt.xlabel('')
plt.ylabel('')
plt.savefig('Тяжесть.png');
Почти 80% ДТП в Екатеринбургне - легкие и менее 10% - с погибшими.
df_cat = (
df.groupby('category', as_index=False)
.agg({'id' : 'count'})
.sort_values(by ='id',ascending=False)
)
clrs = ['#bcbcbc' if x < 100 else '#5a9bc5' for x in df_cat['id']]
sns.barplot(data = df_cat, x='id', y='category', palette=clrs)
plt.xlabel('')
plt.ylabel('')
plt.title('Типы ДТП')
plt.savefig('Типы.png', dpi=90, bbox_inches='tight');
df.participants_count.value_counts().to_frame()
| participants_count | |
|---|---|
| 2 | 678 |
| 3 | 229 |
| 4 | 76 |
| 1 | 33 |
| 5 | 28 |
| 6 | 7 |
| 8 | 3 |
| 7 | 3 |
| 11 | 1 |
Инетесно, что столкновение сразу 4 автомобилей более вероятно, чем аварий с участием 1. И есть инцидент, когда столкнулось сразу 11. Рассмотрим этот случай поближе*
sns.histplot(data = df, x = 'sex', hue = 'severity', stat="percent", multiple="stack" )
plt.title('Виновники ДТП')
plt.xlabel('')
plt.ylabel('')
plt.savefig('Тяжесть+пол.png');
Согласно данным, женщины являются виновницами примерно 20% аварий. Распределение по тяжести - примерно проорционально. Тут наверное было бы правильно сравнить с общим количеством водителей и распределением в нем мужчин и женщин. Мне не удалось найти открытых данных на этот счет.
sns.histplot(data = df, x = 'stage', hue='sex', multiple="stack")
plt.title('Стаж вождения виновников')
plt.xlabel('')
plt.ylabel('')
plt.savefig('Стаж.png');
Интересно, что у женщин аварии не так сильно коррелируют со стажем вождения как у мужчин. А еще женщин со стажем вождения больше 30 лет в Екб почти нет.
В каких районах произошли наши аварии?
list_districts = []
list_points = []
list_polygons = []
list_id = []
for i in range(len(df_polygons)):
for j in range(len(df_history)):
if df_polygons['geometry'][i].contains(df_history['geometry'][j]):
list_districts.append(df_polygons['district'][i])
list_points.append(df_history['geometry'][j])
list_polygons.append(df_polygons['geometry'][i])
list_id.append(df_history['id'][j])
# Создаем таблицу с точками и районами
district_points = pd.DataFrame()
district_points['district'] = list_districts
district_points['geometry_points'] = list_points
district_points['geometry_polygons'] = list_polygons
district_points['id'] = list_id
district_points.head()
| district | geometry_points | geometry_polygons | id | |
|---|---|---|---|---|
| 0 | Академический | POINT (60.51411 56.80216) | POLYGON ((60.3838205 56.8001734, 60.3840566 56... | 1784084 |
| 1 | Академический | POINT (60.471776 56.788869) | POLYGON ((60.3838205 56.8001734, 60.3840566 56... | 2605487 |
| 2 | Академический | POINT (60.506312 56.794339) | POLYGON ((60.3838205 56.8001734, 60.3840566 56... | 2646031 |
| 3 | Академический | POINT (60.494821 56.80512) | POLYGON ((60.3838205 56.8001734, 60.3840566 56... | 2646114 |
| 4 | Академический | POINT (60.444546 56.802999) | POLYGON ((60.3838205 56.8001734, 60.3840566 56... | 2646161 |
Теперь добавим район по id в нашу таблицу.
df = df.merge(district_points[['id', 'district']], on='id')
df_history = df_history.merge(district_points[['id', 'district']], on='id')
display(df.shape)
display(df_history.shape)
df['district']
(1058, 32)
(7745, 25)
0 Ленинский
1 Ленинский
2 Железнодорожный
3 Орджоникидзевский
4 Ленинский
...
1053 Ленинский
1054 Верх-Исетский
1055 Ленинский
1056 Орджоникидзевский
1057 Железнодорожный
Name: district, Length: 1058, dtype: object
districts_df = (
df.groupby('district',
as_index=False)
.agg({'id' : 'count'})
.sort_values(by ='id', ascending=False)
)
clrs = ['#bcbcbc' if x < 150 else '#5a9bc5' for x in districts_df['id']]
sns.barplot(data = districts_df, y='district', x='id', palette=clrs)
plt.title('Количество ДТП в разных районах')
plt.xlabel('')
plt.ylabel('')
;
''
Если посмотреть на карту, увидим что Академический - просто самый маленький по площади, а Чкаловский - самый большой.
Посмотрим так же, где более распространены ДТП со смертельным исходом.
districts_df = (
df.groupby(['district', 'severity'],
as_index=False)
.agg({'id' : 'count'})
.sort_values(by =['id', 'district'])
)
sns.barplot(data = districts_df, y='district', x='id', hue='severity', palette="blend:#7AB,#EDA")
plt.title('Количество ДТП в разных районах')
plt.xlabel('')
plt.ylabel('');
В Академическом ДТП со смертельным исходом почти нет, потому что это маленький спальный район. А в Октябрьском их доля больше всего - не удиительно, это во-первых, большой по площади район, а во сторых, в нем расположена дорога в Аэропорт и еще несколько автострад с высоким скоростным режимом.
district_pivot = (
df_history.pivot_table(
index = ['year', 'district'],
values = 'id',
aggfunc='count')
.reset_index()
)
district_pivot
| year | district | id | |
|---|---|---|---|
| 0 | 2015 | Академический | 26 |
| 1 | 2015 | Верх-Исетский | 175 |
| 2 | 2015 | Железнодорожный | 88 |
| 3 | 2015 | Кировский | 100 |
| 4 | 2015 | Ленинский | 118 |
| ... | ... | ... | ... |
| 59 | 2022 | Кировский | 113 |
| 60 | 2022 | Ленинский | 146 |
| 61 | 2022 | Октябрьский | 140 |
| 62 | 2022 | Орджоникидзевский | 148 |
| 63 | 2022 | Чкаловский | 202 |
64 rows × 3 columns
fig, ax=plt.subplots(figsize=(15, 5), dpi=150)
ax = sns.lineplot(
data = district_pivot,
x = 'year',
y='id',
hue='district')
plt.title('Динамика числа аварий 2015-2022 гг')
plt.xlabel('')
plt.ylabel('');
plt.savefig('Динамина_районы.png')
Видим, что динамика по районам похожая: с 2016 по 2017 годах везде было снижение, а с 17 года пошел рост ДТП, c 2021 года у всех кроме Чкаловского района спад, но в то же время и Академический начал расти.
Посмотрим на динамику смертельных исходов.
# добавим статистику о тяжести ДТП
district_pivot = (
df_history.pivot_table(
index = ['year', 'district', 'severity'],
values = 'id',
aggfunc='count')
.reset_index()
)
# построим графики отдельно для каждого района за все время
plt.figure(figsize=(15, 12), dpi=150)
plt.subplots_adjust(hspace=0.5)
plt.suptitle("Динамика серьезности ДТП по районам", fontsize=18, y=0.95)
ncols = 2
nrows = len(districts) // ncols + (len(districts) % ncols > 0)
for n, district in enumerate(districts):
# add a new subplot iteratively using nrows and cols
ax = plt.subplot(nrows, ncols, n + 1)
# filter df and plot ticker on the new subplot axis
ax = sns.lineplot(
data = district_pivot[district_pivot["district"] == district],
x = 'year',
y='id',
hue='severity')
# chart formatting
ax.set_title(district)
#ax.get_legend().remove()
ax.set_xlabel("")
ax.set_ylabel("")
plt.savefig('динамика_тяжесть.png', dpi=90, bbox_inches='tight');
df['health_status'].value_counts(normalize=True)
Не пострадал 0.690926 Раненый, находящийся (находившийся) на амбулаторном лечении, либо в условиях дневного стационара 0.184310 Раненый, находящийся (находившийся) на стационарном лечении 0.058601 Получил телесные повреждения с показанием к лечению в медицинских организациях (кроме разовой медицинской помощи) 0.043478 Скончался на месте ДТП до приезда скорой медицинской помощи 0.009452 Получил травмы с оказанием разовой медицинской помощи, к категории раненый не относится 0.005671 Скончался в течение 1 суток 0.002836 Скончался при транспортировке 0.001890 Получил телесные повреждения с показанием к лечению в медицинских организациях, фактически лечение не проходил, к категории раненый не относится 0.000945 Скончался на месте ДТП по прибытию скорой медицинской помощи, но до транспортировки в мед. организацию 0.000945 Скончался в течение 10 суток 0.000945 Name: health_status, dtype: float64
70% участвовавших в ДТП не пострадали. Посмотрим, как оставшиеся распределены по районам города.
trauma = (
df.query('health_status != "Не пострадал"')
.groupby('district',
as_index=False)
.agg({'id' : 'count'})
.sort_values(by =['id', 'district'], ascending=False)
)
clrs = ['#bcbcbc' if x < 50 else '#5a9bc5' for x in trauma['id']]
sns.barplot(data = trauma, y='district', x='id', palette=clrs)
plt.title('Количество ДТП с травмами в разных районах')
plt.xlabel('')
plt.ylabel('')
plt.savefig('травмы.png', dpi=90, bbox_inches='tight');
Меньше всего травм - в Академическом, и больше всего - в Чкаловском.
Посмотрим как наша статистика выглядит на карте.
incidents_accident = folium.map.FeatureGroup()
latitudes = list(df.lat)
longitudes = list(df.long)
labels = list(df.severity)
for lat, lng, label in zip(latitudes, longitudes, labels):
if label == 'С погибшими':
folium.features.CircleMarker(
location = [lat, lng],
radius=3,
popup = label,
color='darkred',
fill=True,
fill_opacity=0.6
).add_to(ekb_map)
elif label =='Тяжёлый':
folium.features.CircleMarker(
location = [lat, lng],
radius=3,
popup = label,
color='blue',
fill=True,
fill_opacity=0.6).add_to(ekb_map)
ekb_map.add_child(incidents_accident)
ekb_map
#testing
incidents_accident = folium.map.FeatureGroup()
latitudes = list(df.lat)
longitudes = list(df.long)
labels = list(df.severity)
for lat, lng, label in zip(latitudes, longitudes, labels):
if label == 'С погибшими':
folium.features.CircleMarker(
location = [lat, lng],
radius=3,
popup = label,
color='darkred',
fill=True,
fill_opacity=0.6
).add_to(ekb_map)
elif label =='Тяжёлый':
folium.features.CircleMarker(
location = [lat, lng],
radius=3,
popup = label,
color='blue',
fill=True,
fill_opacity=0.6).add_to(ekb_map)
ekb_map.add_child(incidents_accident)
image = ('showcast_fire_legend.png')
FloatImage(image, bottom=18, left=1).add_to(m)
ekb_map
Можно посмотреть на тепловой карте, в какие часы в городе происходит больше аварий.
ekb_map = folium.Map(location=[56.8519, 60.6122], zoom_start = 10)
# создадим вложенный список с часами и координатами
hour_list = [[] for _ in range(24)]
for lat,log,hour in zip(df.lat,df.long,df.datetime.dt.hour):
hour_list[hour].append([lat,log])
# индекс-бар
index = [str(i)+' Hours' for i in range(24)]
# добавим на карту
HeatMapWithTime(hour_list, index).add_to(ekb_map)
# добавим границы районов
for _, d in df_polygons.iterrows():
sim_geo = gpd.GeoSeries(d['geometry']).simplify(tolerance=0.001)
geo_j = sim_geo.to_json()
geo_j = folium.GeoJson(data=geo_j,
style_function=lambda x: {'fillColor': 'orange'})
folium.Popup(d['district']).add_to(geo_j)
geo_j.add_to(ekb_map)
ekb_map
Нарисуем так же тепловую карту, на которой будут видны опасные районы (места, где сконцентрированы дтп).
ekb_map = folium.Map(location=[56.8519, 60.6122], zoom_start = 10)
# координаты
lat = df.lat.tolist()
lng = df.long.tolist()
# добавим на карту
HeatMap(list(zip(lat, lng))).add_to(ekb_map)
# добавим границы районов
for _, d in df_polygons.iterrows():
sim_geo = gpd.GeoSeries(d['geometry']).simplify(tolerance=0.001)
geo_j = sim_geo.to_json()
geo_j = folium.GeoJson(data=geo_j,
style_function=lambda x: {'fillColor': 'orange'})
folium.Popup(d['district']).add_to(geo_j)
geo_j.add_to(ekb_map)
ekb_map
Если внимательно рассмотреть места аварий, можно сделать следующие выводы:
# создадим слои для аварий с разными сезонами
ekb_map = folium.Map(location=[56.8519, 60.6122], zoom_start = 10)
# добавим границы районов
for _, d in df_polygons.iterrows():
sim_geo = gpd.GeoSeries(d['geometry']).simplify(tolerance=0.001)
geo_j = sim_geo.to_json()
geo_j = folium.GeoJson(data=geo_j,
style_function=lambda x: {'fillColor': 'orange'})
folium.Popup(d['district']).add_to(geo_j)
geo_j.add_to(ekb_map)
ekb_map
mCluster1 = MarkerCluster(name = "Winter").add_to(ekb_map)
lat1 = df.query('season == "winter"')['lat'].tolist()
lng1 = df.query('season == "winter"')['long'].tolist()
location1 = list(zip(lat1, lng1))
for point in range(0, len(location1)):
folium.Marker(location1[point]).add_to(mCluster1)
mCluster2 = MarkerCluster(name = "Spring").add_to(ekb_map)
lat2 = df.query('season == "spring"')['lat'].tolist()
lng2 = df.query('season == "spring"')['long'].tolist()
location2 = list(zip(lat2, lng2))
for point in range(0, len(location2)):
folium.Marker(location2[point]).add_to(mCluster2)
mCluster3 = MarkerCluster(name = "Summer").add_to(ekb_map)
lat3 = df.query('season == "summer"').lat.tolist()
lng3 = df.query('season == "summer"').long.tolist()
location3 = list(zip(lat3, lng3))
for point in range(0, len(location3)):
folium.Marker(location3[point]).add_to(mCluster3)
mCluster4 = MarkerCluster(name = "Autumn").add_to(ekb_map)
lat4 = df.query('season == "autumn"').lat.tolist()
lng4 = df.query('season == "autumn"').long.tolist()
location4 = list(zip(lat4, lng4))
for point in range(0, len(location4)):
folium.Marker(location4[point]).add_to(mCluster4)
folium.LayerControl(collapsed=True).add_to(ekb_map)
ekb_map
Опять же трудно интерпретировать сезонность - у нас нет целого месяца. Посмотрим на исторические данные тоже.
season_district = (
df_history.groupby(['district', 'season'],
as_index=False)
.agg({'id': 'count'})
)
sns.barplot(data = season_district, x='id', y='district', hue='season', palette='Spectral')
plt.title('Распределение по районам и сезонам')
plt.xlabel('')
plt.ylabel('');
Этот график показывает нам те же данные что и по городу вцелом, тут ни один район не отличается особой оригинальностью: осенью и весной везде больше аварий.
ekb_map = folium.Map(location=[56.8519, 60.6122], zoom_start = 10)
# добавим границы районов
for _, d in df_polygons.iterrows():
sim_geo = gpd.GeoSeries(d['geometry']).simplify(tolerance=0.001)
geo_j = sim_geo.to_json()
geo_j = folium.GeoJson(data=geo_j,
style_function=lambda x: {'fillColor': 'orange'})
folium.Popup(d['district']).add_to(geo_j)
geo_j.add_to(ekb_map)
ekb_map
incidents_accident = folium.map.FeatureGroup()
latitudes = list(df.lat)
longitudes = list(df.long)
labels = list(df.sex)
for lat, lng, label in zip(latitudes, longitudes, labels):
if label == 'Мужской':
folium.features.CircleMarker(
location = [lat, lng],
radius=2,
popup = label,
color='blue',
fill=True,
fill_opacity=0.6
).add_to(ekb_map)
elif label =='Женский':
folium.features.CircleMarker(
location = [lat, lng],
radius=2,
popup = label,
color='red',
fill=True,
fill_opacity=0.6).add_to(ekb_map)
ekb_map.add_child(incidents_accident)
ekb_map
Мы провели исследование транспортной безопасности Екатеринбурга в 2022 году и также изучили исторические данные.
В предоставленных данных нехватало адресов - мы достали их по координатам при помощи Open street map и geopy.
Выяснили что в нашем источнике данные с 2015 года, что позволяет нам проанализировать достаточно большой объем данных о ДТП - почти за 8 лет, но из-за того что в наших данных отсутствует декабрь 2022 года - мы не можем в полной мере рассмотреть сезонность и распределение аварий по месяцам. Скорее всего, следует дождаться окончания года и перезалить данные.
Пока что мы пришли к следующим выводам:
Дополнительно можно изучить: